package sample;

import static rescuecore2.misc.Handy.objectsToIDs;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Iterator;


import rescuecore2.worldmodel.EntityID;
import rescuecore2.worldmodel.ChangeSet;
import rescuecore2.messages.Command;
import rescuecore2.log.Logger;

import rescuecore2.standard.entities.StandardEntity;
import rescuecore2.standard.entities.StandardEntityURN;
import rescuecore2.standard.entities.Building;
import rescuecore2.standard.entities.Refuge;
import rescuecore2.standard.entities.FireBrigade;
import rescuecore2.standard.messages.AKSpeak;
import rescuecore2.worldmodel.WorldModel;
import rescuecore2.worldmodel.WorldModelListener;

/**
 * A sample fire brigade agent.
 */
public class SampleFireBrigade extends AbstractSampleAgent<FireBrigade> {
	private static final String MAX_WATER_KEY = "fire.tank.maximum";
	private static final String MAX_DISTANCE_KEY = "fire.extinguish.max-distance";
	private static final String MAX_POWER_KEY = "fire.extinguish.max-sum";

	private final double threshold = -1; // LA-DCOP
	private final int MAX_TAREFAS = 1;

	private int maxWater;
	private int maxDistance;
	private int maxPower;

	private final int MAX_FIERYNESS = 4;

	private Collection<EntityID> targets;
	private List<GeneralData> selectedTargets;
	private List<GeneralData> notSelected;
	private List<GeneralData> tokenBuildings;
	private HashMap<Integer, Integer> inDoubt;
	
	private Token token;

	private StandardEntity[] fireAgents;

	private EntityID activeTask;

	// Construtor padrão
	public SampleFireBrigade() {
		targets = new ArrayList();
		selectedTargets = new ArrayList();
		notSelected = new ArrayList();
		tokenBuildings = new ArrayList<GeneralData>();
		inDoubt = new HashMap<Integer, Integer>();
	}

	@Override
	public String toString() {
		return "Sample fire brigade";
	}

	@Override
	protected void postConnect() {
		super.postConnect();
		model.indexClass(StandardEntityURN.BUILDING, StandardEntityURN.REFUGE);
		maxWater = config.getIntValue(MAX_WATER_KEY);
		maxDistance = config.getIntValue(MAX_DISTANCE_KEY);
		maxPower = config.getIntValue(MAX_POWER_KEY);
		Logger.info("Sample fire brigade connected: max extinguish distance = "
				+ maxDistance + ", max power = " + maxPower + ", max tank = "
				+ maxWater);
		// Seta os listeners para o agente
		start();
	}

	// Seta os listeners para os agentes, responsável pela percepção do mundo
	public void start() {
		activeTask = null;

		// grava num array todos os agentes FireBrigade para comunicacao
		Collection<StandardEntity> allFireAgents = model
				.getEntitiesOfType(StandardEntityURN.FIRE_BRIGADE);
		fireAgents = new StandardEntity[allFireAgents.size()];
		allFireAgents.toArray(fireAgents);

		model.addWorldModelListener(new WorldModelListener<StandardEntity>() {

			@Override
			public void entityAdded(WorldModel<? extends StandardEntity> model,
					StandardEntity e) {
				Logger.debug("Aqui added: " + e.getStandardURN());
				// Se uma nova construção em chamas for observada pelo agente,
				// adiociona-a a sua lista de alvos
				if (e.getStandardURN() == StandardEntityURN.BUILDING) {
					Logger.debug("construcao");
					if (((Building) e).isOnFire())
						Logger.debug("construcao incendiada");
//						targets.add(e); // Add to target list if it is on fire
				}
			}

			@Override
			public void entityRemoved(
					WorldModel<? extends StandardEntity> model, StandardEntity e) {
				Logger.debug("Aqui removed");
				// Verifica-se as construções em chamas em caso do mudança nas
				// mesmas
				if (e.getStandardURN() == StandardEntityURN.BUILDING) {
//					if (((Building) e).isOnFire())
////						targets.add(e); // Se estiver em chamas adiciona à lista
//					else
//						targets.remove(e); // Caso contrário, remove
				}
			}
		});
	}

	@Override
	protected void think(int time, ChangeSet changed, Collection<Command> heard) {
		if (time == config
				.getIntValue(kernel.KernelConstants.IGNORE_AGENT_COMMANDS_KEY)) {
			// Subscribe to channel 1
			sendSubscribe(time, 1);
			Logger.debug("Inscrito no canal.");
		}
                // Verifica a lista de objetos ouvidos
		for (Command next : heard) {
			Logger.debug("Heard " + next);
			if (next instanceof AKSpeak) {
				AKSpeak sp = (AKSpeak) next;
				this.token = new Token();
				if (this.token.decodeToken(sp.getContent(), model) == 1) {
					for (Iterator<GeneralData> it = this.token.getData().iterator(); it.hasNext();) {
						GeneralData gd = it.next();
						if (!isOnTargets(gd.getID()) && this.activeTask != getId(gd.getID().getValue()))
								this.tokenBuildings.add(gd);
					}
				} else if (this.token.getType() == 11) { // Token para evitar duplicacao de active task
					GeneralData gd = this.token.getData().get(0);
					if (this.activeTask == getId(gd.getID().getValue()) &&
							me().getID().getValue() != token.getSender().getValue()) {
						List<EntityID> path = search
							.aStarSearch(me().getPosition(), activeTask);
						if (path.size() > gd.getValue()) {
							Logger.debug("Giving up task " + activeTask + ", another agente is more capable ...");
							this.inDoubt.put(new Integer(activeTask.getValue()), new Integer(-2));
							removeBuildingFromTargets(activeTask);
							activeTask = null;
						}
					} else if (me().getID().getValue() != token.getSender().getValue()) {
						Logger.debug("Putting task " + gd.getID() + " on dead list.");
						this.inDoubt.put(new Integer(gd.getID().getValue()), new Integer(-2));
					}
				}
			}			
		}
		FireBrigade me = me();

		// Are we currently filling with water?
		if (me.isWaterDefined() && me.getWater() < maxWater
				&& location() instanceof Refuge) {
			Logger.info("Filling with water at " + location());
			sendRest(time);
			return;
		}
		
		// Are we out of water?
		if (me.isWaterDefined() && me.getWater() == 0) {
			// Head for a refuge
			List<EntityID> path = search.aStarSearch(me().getPosition(),
					refugeIDs);
			if (path != null) {
				Logger.info("Moving to refuge");
				sendMove(time, path);
				return;
			} else {
				Logger.debug("Couldn't plan a path to a refuge.");
				path = randomWalk();
				Logger.info("Moving randomly");
				sendMove(time, path);
				return;
			}
		}
		
		targets = getBurningBuildings();

		// Impede que o agente abandone uma tarefa que ele já esteja realizando
		int distance;
		if (activeTask != null) {
			distance = model.getDistance(me().getID(), activeTask);
			Building build = (Building) model.getEntity(activeTask);
			
			Logger.debug("Active task ID = " + activeTask);
			
			if (distance < maxDistance && distance > -1) {
				Logger.debug("Can extinguish active task from here...");
				if (build.isFierynessDefined()) {
					Logger.debug("... and I can see the target ...");
					if (build.isOnFire() && !hadTrouble(build.getID())) {
						if (build.getFieryness() < MAX_FIERYNESS) {
							Logger.debug("... and the target is burning yet, fairyness = " + build.getFieryness() + ". Let's extinguish!");
							sendExtinguish(time, activeTask, maxPower);
							inDoubt.put(new Integer(activeTask.getValue()), build.getTemperature());
							
							// Avisa que ira atender este chamado e que a distancia ate ele é 0.
							Logger.debug("Avisando outros agentes que farei esta task.");
							Token tk = new Token();
							tk.getData().add(new GeneralData(activeTask, 0));
							sendSpeak(time, 1, tk.encondeToken(model, me().getID(), 11));
						}
					} else {
						Logger.debug("... the fire is too small, canceling active task.");
						removeBuildingFromTargets(activeTask);
						
						this.activeTask = null;
					}
				} else {
					Logger.debug("... but I can't see it from here.");
					List<EntityID> path = search
						.aStarSearch(me().getPosition(), build.getID()); // Find a path
					if (path != null) {
						Logger.debug("Found a path to task... moving.");
						sendMove(time, path);
						return;
					}
				}
				return;
			} 
			Logger.debug("A caminho de " + build.getID() + ", distancia: " + distance);
			List<EntityID> path = search
				.aStarSearch(me().getPosition(), build.getID()); // Find a path
			if (path != null) {
				Logger.debug("Found a path to task... moving.");
				sendMove(time, path);
				return;
			}
			
		}
		
		// Se houver prováveis alvos, aplica o LA-DCOP
		if (this.activeTask == null && (!targets.isEmpty() || !this.tokenBuildings.isEmpty())) {
			Logger.debug("No task defined, entering in LA-DCOP");
			LA_DCOP(time);
			this.tokenBuildings.clear(); // Nao mais necessario
		}	
		
		removeApagados();
		notSelected.clear();
		

		act(time, changed);

		targets.clear();
		
		//removeApagados();

		// se nao sabe o que fazer explora
		if (this.selectedTargets.isEmpty()) {
			List<EntityID> path = null;
			Logger.debug("Couldn't plan a path to a fire.");
			path = randomWalk();
			Logger.info("Aqui Moving randomly");
			sendMove(time, path);
			return;
		}
		
		Logger.info("Final Think");
		
	}

	// Envia o token através do sendTell
	public void enviaToken(int time) {
		sendSpeak(time, 1, token.encondeToken(model,me().getID(),1));
	}

	// Remove as construções que estavam queimando mas já foram apagadas da
	// lista de alvos
	private void removeApagados() {
		for (Iterator it = targets.iterator(); it.hasNext();) {
			Building next = (Building) model.getEntity( (EntityID) it.next());
			if (!next.isOnFire())
				it.remove();
		}
	}

//	 private void explore(int time){
//		 Collection<StandardEntity> allRoads =
//		 model.getEntitiesOfType(StandardEntityURN.ROAD);
//		 Road[] roads = new Road[allRoads.size()];
//		 allRoads.toArray(roads);
//		
//		 Road target = (Road)roads[(int)(Math.random()*roads.length)];
//		
////		 SampleSearch busca = new SampleSearch(model);
//		 List<EntityID> path = search.aStarSearch(me().getID(), target.getID());
//		 if (path == null)
//			 path = randomWalk();
//		 sendMove(time, path);
//	 }

	private void act(int time, ChangeSet changed) {
		if (selectedTargets.isEmpty()) {
			Logger.info("No tasks founded or defined");
			return;
		}

		Collections.sort(selectedTargets, new Comparar_Fieryness());

		// Find a valid target
		for (Iterator<GeneralData> it = selectedTargets.iterator(); it.hasNext();) {
			
			GeneralData gd = it.next();
			
			// Por alguma razao ele setou a ultima tarefa como a atual, o que constitui provavelmente um erro.
//			if (gd.getID() == this.lastTask) {
//				Logger.debug("Last task is equal a selected task, skipping...");
//				continue;
//			}
			
			int distancia = model.getDistance(me().getID(), gd.getID());
			Building b = (Building) model.getEntity(gd.getID());
			if (distancia == -1) {
				System.err.println("PANIC! I don't know where I am!");
			}
			else if (model.getDistance(me().getID(), getId(gd.getID().getValue())) < maxDistance && b.isFierynessDefined() && !hadTrouble(gd.getID())) {
				Logger.debug("Task in range and with defined fieryness");
				if (b.getFieryness() < MAX_FIERYNESS) {
					activeTask = getId(gd.getID().getValue());
					
					// Avisa que ira atender este chamado e que a distancia ate ele é 0.
					Logger.debug("Avisando outros agentes que farei esta task.");
					Token tk = new Token();
					tk.getData().add(new GeneralData(activeTask, 0));
					sendSpeak(time, 1, tk.encondeToken(model, me().getID(), 11));
					
					inDoubt.put(new Integer(activeTask.getValue()), new Integer(-1));
					sendExtinguish(time, gd.getID(), maxPower);
					inDoubt.put(new Integer(activeTask.getValue()), b.getTemperature());
					Logger.debug("Send Extinguish to " + gd.getID() + " fieryness = " + gd.getValue());
					return;
				}
				continue;
			} else if(anotherAgentTask(getId(gd.getID().getValue()))) continue;
			
			if (gd.getValue() >= MAX_FIERYNESS) 
				removeBuildingFromTargets(gd.getID());

			// Drive to the first entrance of the building
			List<EntityID> path = search
					.aStarSearch(me().getPosition(), gd.getID()); // Find a path

			if (path != null) {
				Logger.debug("Found a path to task... moving.");
				// We've found a path. Hooray!
				// Send a move command and we're finished
				activeTask = getId(gd.getID().getValue());
				
				// Avisa que ira atender este chamado e que a distancia ate ele é o tamanho do Path.
				Logger.debug("Avisando outros agentes que farei esta task.");
				Token tk = new Token();
				tk.getData().add(new GeneralData(activeTask, path.size()));
				sendSpeak(time, 1, tk.encondeToken(model, me().getID(), 11));
				
				inDoubt.put(new Integer(activeTask.getValue()), new Integer(-1));
				sendMove(time, path);
				return;
			}
		}
	}

	// Algoritmo LA_DCOP
	private void LA_DCOP(int time) {
		// Decide aceitar ou nao cada tarefa conhecida
		int num_aceitas = 0;
		List<GeneralData> buildingTargets = mergeTargetsAndTokens();
		
		// Distância do alvo mais distante
		double[] maximos = calcularMaximos(buildingTargets);
		
		for (Iterator<GeneralData> it = buildingTargets.iterator(); it.hasNext();) {
			GeneralData task = it.next();
			double competencia;
			// Se só existe um alvo, sua competência é máxima (1)
			if (buildingTargets.size() == 1) {
				competencia = 1;
			} else {
				// Senão, calcula sua competência
				int distancia = model.getDistance(me().getID(), task.getID());
				
				int fogo = task.getValue();
				// Caso o método getDistance retorne -1, significa que me() ou
				// task são null. Neste caso a competência é 0
				if (distancia == -1) {
					competencia = 0;
				}
				//
				else {
					competencia = (((maximos[0] - distancia) / maximos[0]) + ((maximos[1] - fogo) / maximos[1])) / 2;
				}
			}
			Logger.debug("Tarefa ID=" + task.getID() + " Fieryness=" + task.getValue());
			Logger.debug("Competencia: " + competencia);
			
			if (threshold < competencia && num_aceitas < MAX_TAREFAS && !anotherAgentTask(task.getID())) {
				num_aceitas++;
				selectedTargets.add(task);
				Logger.info(task.getID() + " added");
			} else {
				notSelected.add(task);
				Logger.info(task.getID() + " refused");
			}
		}
		if (notSelected.size() > 0) {
			if (this.token == null)
				this.token = new Token();
			token.setData(notSelected);
			// comunicar aos outros agentes as tarefas NAO aceitas
			enviaToken(time);
		}
	}
	
	private List<GeneralData> mergeTargetsAndTokens() {
		List<GeneralData> result = new LinkedList<GeneralData>();
		
		for (Iterator<EntityID> it = this.targets.iterator(); it.hasNext();) {
			Building b = (Building) model.getEntity(it.next());
			result.add(new GeneralData(b.getID(), b.getFieryness()));
		}
		result.addAll(this.tokenBuildings);
		
		return result;
	}
	
	private boolean hadTrouble(EntityID id) {
		
		Building b = (Building) model.getEntity(getId(id.getValue()));
		
		int oldTemp = -1;
		if (inDoubt.get(new Integer(id.getValue())) != null) {
			oldTemp = inDoubt.get(new Integer(id.getValue()));
		}
		
		if (oldTemp == -2) return true;
		
		Logger.debug("... old temperature for this is " + oldTemp);
		int newTemp = b.getTemperature();
		
		if (oldTemp == newTemp && oldTemp != -1) {
			return true;
		}
		
		return false;
	}
	
	private boolean anotherAgentTask(EntityID id) {
		if (inDoubt.get(new Integer(id.getValue())) != null) {
			if (inDoubt.get(new Integer(id.getValue())) == -2)
				return true;
			else
				return false;
		} else
			return false;
	}
	
	private void removeBuildingFromTargets(EntityID id) {
		for (Iterator<GeneralData> it = this.selectedTargets.iterator(); it.hasNext();) {
			GeneralData gd = it.next();
			if (gd.getID() == id) {
				it.remove();
				return;
			}
		}
	}
	
	private boolean isOnTargets(EntityID id) {
		List<GeneralData> listAllTargets = new LinkedList<GeneralData>();
		listAllTargets.addAll(this.selectedTargets);
		listAllTargets.addAll(this.tokenBuildings);
		
		for (Iterator<GeneralData> it = listAllTargets.iterator(); it.hasNext();) {
			GeneralData gd = it.next();
			if (gd.getID() == id) {
				return true;
			}
		}
		return false;
	}
	
	private boolean isInChangeSet(ChangeSet changed, EntityID id) {
		
		for (EntityID next : changed.getChangedEntities()) {
        	
        	if (model.getEntity(next) instanceof Building) {
	        	Building b = (Building) model.getEntity(next);
	        	
	        	if (b.getID() == id)
	        		return true;
        	}
        }
		
		 return false;
	}
	
	private EntityID getId(int id) {
		return model.getEntity(new EntityID(id)).getID();
	}


	// Calcula a distância do alvo mais longe do agente
	private double[] calcularMaximos(List<GeneralData> allTargets) {
		// Vetor onde guarda-se a distância e a "ardência" máxima
		double maximos[] = new double[2];
		// maximos[0] para distância e maximos[1] para "ardência"
		maximos[0] = 0;
		maximos[1] = 0;
		for (Iterator<GeneralData> it = allTargets.iterator(); it.hasNext();) {
			GeneralData gd = it.next();

			double distancia = model.getDistance(me().getID(), gd.getID());
			int fogo = gd.getValue();
			// Caso o método getDistance retorne -1, significa que b é nulo e,
			// portanto defina a distância como 10000
			if (distancia == -1)
				distancia = 10000;

			if (distancia > maximos[0])
				maximos[0] = distancia;

			// A partir de 5 de "ardência", significa que o prédio não está
			// pegando fogo
			if (fogo > maximos[1] && fogo < 5)
				maximos[1] = fogo;
		}
		return maximos;
	}

	@Override
	protected EnumSet<StandardEntityURN> getRequestedEntityURNsEnum() {
		return EnumSet.of(StandardEntityURN.FIRE_BRIGADE);
	}

	private Collection<EntityID> getBurningBuildings() {
		Collection<StandardEntity> e = model
				.getEntitiesOfType(StandardEntityURN.BUILDING);
		List<Building> result = new ArrayList<Building>();
		for (StandardEntity next : e) {
			if (next instanceof Building) {
				Building b = (Building) next;
				if (b.isOnFire() && b.getFieryness() < MAX_FIERYNESS) {
					result.add(b);
				}
			}
		}
		// Sort by distance
		Collections.sort(result, new DistanceSorter(location(), model));
		return objectsToIDs(result);
	}

	private List<EntityID> planPathToFire(Collection<EntityID> target) {
		// Try to get to anything within maxDistance of the target
		// Collection<StandardEntity> targets = model.getObjectsInRange(target,
		// maxDistance);
		// if (targets.isEmpty()) {
		// return null;
		// }
		// return search.aStarSearch(me().getPosition(), objectsToIDs(targets));
		return search.aStarSearch(me().getPosition(), target);
	}

	private class Comparar_Fieryness implements Comparator<Object> {
		@Override
		public int compare(Object o1, Object o2) {
			GeneralData b1 = (GeneralData) o1;
			GeneralData b2 = (GeneralData) o2;
			int f1 = b1.getValue();
			int f2 = b2.getValue();
			double d1, d2;
			d1 = model.getDistance(me().getID(), b1.getID());
			d2 = model.getDistance(me().getID(), b2.getID());
			if (d1 == -1 || d2 == -1) {
				d1 = 0;
				d2 = 0;
			}
			if (f1 == f2) {
				if (d1 < d2)
					return -1;
				else if (d1 > d2)
					return 1;
				else
					return 0;
			} else
				return f1 - f2;
		}

	}
}
